package edu.gatech.math2605.kzhou33.client;
import java.util.ArrayList;
import org.vaadin.gwtgraphics.client.DrawingArea;
import org.vaadin.gwtgraphics.client.shape.Path;
import org.vaadin.gwtgraphics.client.shape.Rectangle;
import org.vaadin.gwtgraphics.client.shape.Text;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.gwt.user.client.ui.VerticalPanel;
public class Pinball implements EntryPoint {
private double angleLimit = 360;
private int WIDTH = 500, HEIGHT = 500, circleRadius = 60, ballRadius = 2,
side = 360; // radius < side/2
private ArrayList<Integer> lastCircle = new ArrayList<Integer>();
private DrawingArea gwtCanvas;
private VirtualCircle c1, c2, c3, ball;
private FastCircle ballFast;
private TextBox angleBox, sideBox, circleRadiusBox, ballRadiusBox;
private FlexTable infoLabel, testLabel;
private Path[] path;
private DockLayoutPanel dlp;
private boolean isSlow = true;
private double[] max = new double[2];
private double initAngle, angle;
private int hits = 0, pathNum = 0;
private int[] freq = new int[1000];
private TextBox numTrials;
private CheckBox randomOrEqual;
private ArrayList<Result> results = new ArrayList<Result>();
private Result tempResult;
// 85.88801
public void onModuleLoad() {
lastCircle.add(0);
max[0] = max[1] = 0;
dlp = new DockLayoutPanel(Unit.PCT);
VerticalPanel vp = createSidePanel();
HorizontalPanel ap = createCanvas(WIDTH, HEIGHT, circleRadius,
ballRadius, side);
dlp.addWest(new ScrollPanel(vp), 20);
dlp.add(new ScrollPanel(ap));
RootLayoutPanel.get().add(dlp);
}
private VerticalPanel createSidePanel() {
VerticalPanel vp = new VerticalPanel();
Button start = new Button("Start");
start.addClickHandler(new startHandler("start"));
Button reset = new Button("Reset/ Update");
reset.addClickHandler(new startHandler("reset"));
Button clear = new Button("Clear Trail");
clear.addClickHandler(new startHandler("clear"));
Button special = new Button("Run Many");
special.addClickHandler(new startHandler("special"));
sideBox = new TextBox();
circleRadiusBox = new TextBox();
ballRadiusBox = new TextBox();
angleBox = new TextBox();
sideBox.setText(side + "");
circleRadiusBox.setText(circleRadius + "");
ballRadiusBox.setText(ballRadius + "");
angleBox.setText(String.valueOf(angle));
final TextBox incre = new TextBox();
incre.setText("2");
randomOrEqual = new CheckBox(
"random if checked/ otherwise equally spaced");
angleBox.addKeyDownHandler(new KeyDownHandler() {
public void onKeyDown(KeyDownEvent event) {
if (event.isUpArrow()) {
angle += Double.valueOf(incre.getText());
angleBox.setText(angle + "");
if (validate()) {
run(angle);
}
} else if (event.isDownArrow()) {
angle -= Double.valueOf(incre.getText());
angleBox.setText(angle + "");
if (validate()) {
run(angle);
}
} else if (event.isRightArrow()) {
incre.setText((Double.valueOf(incre.getText()) * (1 / 2d))
+ "");
//angleBox.setText(angleBox.getText().replace(oldChar, newChar)
} else if (event.isLeftArrow()) {
incre.setText((Double.valueOf(incre.getText()) * 2d) + "");
} else if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
if (validate()) {
reset();
run(angle);
}
}
}
});
vp.add(new Label("Pinball Project"));
vp.add(new Label("Created by"));
vp.add(new Label("Kefu Zhou"));
vp.add(new Label("Angle in degrees (0-360)"));
vp.add(angleBox);
vp.add(new Label("Triangle side length"));
vp.add(sideBox);
vp.add(new Label("Circle radius"));
vp.add(circleRadiusBox);
vp.add(new Label("Ball radius (still a point)"));
vp.add(ballRadiusBox);
vp.add(new Label("Increment"));
vp.add(incre);
HorizontalPanel hp = new HorizontalPanel();
hp.add(start);
hp.add(reset);
hp.add(clear);
vp.add(hp);
HorizontalPanel hp2 = new HorizontalPanel();
hp2.add(special);
numTrials = new TextBox();
hp2.add(numTrials);
vp.add(hp2);
vp.add(randomOrEqual);
final ToggleButton showFreq = new ToggleButton("Show Freq Table",
"Hide Freq Table");
showFreq.addClickHandler(new ClickHandler() {
private FreqPopup fp;
public void onClick(ClickEvent event) {
if (showFreq.isDown()) {
if (fp == null) {
fp = new FreqPopup(freq);
// fp.setAutoHideEnabled(true);
} else {
fp.update(freq);
}
// fp.setHeight("100%");
fp.show();
fp.center();
} else {
fp.hide();
}
}
});
vp.add(showFreq);
Button topTen = new Button("Show top 10");
vp.add(topTen);
topTen.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
Grid g = null;
PopupPanel pop = null;
if (g == null) {
pop = new PopupPanel();
pop.setAutoHideEnabled(true);
g = new Grid(11, 3);
g.setText(0, 0, "Top");
g.setText(0, 1, "Initial Angle");
g.setText(0, 2, "Circles hit");
pop.add(g);
}
for (int i = 0; i < results.size(); i++) {
g.setText(i + 1, 0, i + 1 + "");
g.setText(i + 1, 1, results.get(i).getInitialAngle() + "");
g.setText(i + 1, 2, results.get(i).printHitSequence());
}
pop.show();
pop.center();
}
});
final CheckBox angleLimitBox = new CheckBox("0-120 instead of 0-360");
angleLimitBox.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
if (angleLimitBox.getValue()) {
angleLimit = 120d;
} else
angleLimit = 360d;
}
});
vp.add(angleLimitBox);
testLabel = new FlexTable();
testLabel.setStyleName("infoLabel");
testLabel.getColumnFormatter().setStyleName(0, "even");
testLabel.getColumnFormatter().setStyleName(1, "odd");
testLabel.getColumnFormatter().setStyleName(2, "even");
testLabel.getColumnFormatter().setStyleName(3, "odd");
testLabel.getColumnFormatter().setStyleName(4, "even");
return vp;
}
private HorizontalPanel createCanvas(int WIDTH, int HEIGHT,
int circleRadius, int ballRadius, int side) { // radius < side/2) {
HorizontalPanel panel = new HorizontalPanel();
double yCTR = HEIGHT / 2;
double xCTR = WIDTH / 2;
double c1_x = xCTR;
double c1_y = yCTR;
double c2_x = xCTR;
double c2_y = yCTR;
double c3_x = xCTR;
double c3_y = yCTR;
c1_x += 0;
c1_y += side * Math.sqrt(3) / 3.0;
c2_x -= side / 2.0f;
c2_y -= side * Math.sqrt(3) / 6.0;
c3_x += side / 2.0f;
c3_y -= side * Math.sqrt(3) / 6.0;
c1 = new VirtualCircle(c1_x, c1_y, circleRadius);
c2 = new VirtualCircle(c2_x, c2_y, circleRadius);
c3 = new VirtualCircle(c3_x, c3_y, circleRadius);
String property = "orange";
c1.setFillColor(property);
c2.setFillColor(property);
c3.setFillColor(property);
// ballRadius = 15;
ball = new VirtualCircle(xCTR, yCTR, ballRadius);
ball.setFillColor("red");
ballFast = new FastCircle(xCTR, yCTR, ballRadius);
gwtCanvas = new DrawingArea(WIDTH, HEIGHT);
gwtCanvas.add(new Rectangle(0, 0, WIDTH, HEIGHT));
gwtCanvas.add(c1);
gwtCanvas.add(c2);
gwtCanvas.add(c3);
gwtCanvas.add(ball);
if (isSlow) {
path = new Path[1];
for (int i = 0; i < path.length; i++) {
path[i] = new Path(WIDTH / 2, HEIGHT / 2);
path[i].setStrokeWidth(1);
path[i].setStrokeColor("blue");
path[i].setFillOpacity(0.00);
gwtCanvas.add(path[i]);
}
path[0].setStrokeColor("black");
// path[1].setStrokeColor("blue");
// path[2].setStrokeColor("blue");
// path[3].setStrokeColor("blue");
// path[4].setStrokeColor("indigo");
}
Text t1 = new Text(c2.getX() - 50, c2.getY(), "Created by");
Text t2 = new Text(c3.getX() - 50, c3.getY(), "Kefu Zhou");
Text c3Text = new Text(c3.getX(), c2.getY() + 20, "3");
Text c2Text = new Text(c2.getX(), c3.getY() + 20, "2");
Text c1Text = new Text(c1.getX(), c1.getY() + 20, "1");
gwtCanvas.add(t1);
gwtCanvas.add(t2);
gwtCanvas.add(c1Text);
gwtCanvas.add(c2Text);
gwtCanvas.add(c3Text);
panel.add(gwtCanvas);
infoLabel = new FlexTable();
infoLabel.setStyleName("infoLabel");
infoLabel.getColumnFormatter().setStyleName(0, "even");
infoLabel.getColumnFormatter().setStyleName(1, "odd");
infoLabel.getColumnFormatter().setStyleName(2, "even");
infoLabel.getColumnFormatter().setStyleName(3, "odd");
infoLabel.getColumnFormatter().setStyleName(4, "even");
createTableHeaders();
VerticalPanel vp = new VerticalPanel();
vp.add(testLabel);
vp.add(infoLabel);
vp.add(new HTML(
"<div><span style=\"text-align: -webkit-left; \"><font color=\"#ff0000\" size=\"2\"><div>Instructions:</div>"
+ "<div>1) Click on angle textbox</div>"
+ "<div>2) Press the up/down arrow key to +/- angle by value specified in increment box</div>"
+ "<div>3) Change increment by changing value in increment box, or press left/right arrow key while on angle box to multiply or divide by 0.1</div>"
+ "<div><br></div>"
+ "<div>Legend:</div>"
+ "<div>Angle Box (degrees): initial angle clockwise from East</div>"
+ "<div>Side Length Box (pixels) : distance from origin of one circle to another circle</div>"
+ "<div>Circle Radius (pixels): Radius of circle</div>"
+ "<div>Ball Radius (pixels): Radius of pinball (does not affect result)</div>"
+ "<div>Increment (degrees): Value to change by when (up,right)/(down/left) is pressed</div>"
+ "<div><br></div>"
+ "<div>Start: Run once with path enabled</div>"
+ "<div>Reset/ Update: redraw canvas area</div>"
+ "<div>Clear: Clears the path</div>"
+ "<div>Run many: Runs simulation for specified iterations</div>"
+ "<div>Checkbox #1: check for random angles, otherwise equally spaced angles</div>"
+ "<div>Show Freq Table: Shows number of results at each hit</div>"
+ "<div>Show Top 10: Shows top twn initial angles and circles hit</div>"
+ "<div>Checkbox #2: check to use range 0-120 instead of 0-360</div>"
+ "</font></span></div>"));
ScrollPanel sp = new ScrollPanel();
sp.add(vp);
sp.setHeight(gwtCanvas.getHeight() + "px");
panel.add(sp);
return panel;
}
public boolean moveObject(VirtualCircle b) {
boolean cancel = false;
// move
b.moveV();
// check collision
if (lastCircle.get(lastCircle.size() - 1) != 1
&& PinballMath.intersect(c1, ball) != -1) {
doPhysics(c1, ball, 1);
} else if (lastCircle.get(lastCircle.size() - 1) != 2
&& PinballMath.intersect(c2, ball) != -1) {
doPhysics(c2, ball, 2);
} else if (lastCircle.get(lastCircle.size() - 1) != 3
&& PinballMath.intersect(c3, ball) != -1) {
doPhysics(c3, ball, 3);
} else {
// hits++;// make it start at 1
freq[hits]++;
logMax();
hits = 0;
if (isSlow) {
path[pathNum].lineTo(
(int) (ball.getVx() + WIDTH * ball.getDx()),
(int) (ball.getVy() + WIDTH * ball.getDy()));
}
ball.setVxy(WIDTH / 2, HEIGHT / 2);
ball.setDxy(0, 0);
cancel = true;
}
return cancel;
}
private void doPhysics(VirtualCircle a, VirtualCircle b, int i) {
hits++;
lastCircle.add(i);
double intersectTime = PinballMath.intersect(a, b);
b.setVxy(b.getVx() + intersectTime * b.getDx(), b.getVy()
+ intersectTime * b.getDy());
// TODO animate, use intersect time
double[] tempReflect = PinballMath.reflect(a, b);
b.setDxy(tempReflect[0], tempReflect[1]);
if (isSlow) {
path[pathNum].lineTo(b.getX(), b.getY());
logToTable(intersectTime, b.getVx(), b.getVy());
}
}
private void logToTable(double intersectTime, double vx, double vy) {
if (isSlow) {
int row = infoLabel.getRowCount();
double old = (row == 1 ? 0 : Double.valueOf(infoLabel.getText(
row - 1, 1)));
infoLabel.setText(row, 0, hits + "");
infoLabel.setText(row, 1, (intersectTime + old) + "");
infoLabel.setText(row, 2, vx + "");
infoLabel.setText(row, 3, vy + "");
infoLabel.setText(row, 4, intersectTime + "");
}
}
private void logMax() {
if (hits >= max[0]) {
lastCircle.remove(0);
tempResult = new Result(initAngle, lastCircle);
results.add(0, tempResult);
while (results.size() > 10) {
results.remove(10);
}
max[0] = hits;
max[1] = initAngle;
}
testLabel.setText(1, 0, max[0] + "");
testLabel.setText(1, 1, max[1] + "");
}
private class startHandler implements ClickHandler {
String source = "";
public startHandler(String source) {
this.source = source;
}
public void onClick(ClickEvent event) {
if (validate()) {
if (source.equals("start")) {
run(angle);
} else if (source.equals("special")) {
isSlow = false;
int numRuns = Integer.valueOf(numTrials.getText());
if (randomOrEqual.getValue()) {
for (int i = 0; i < numRuns; i++) {
runFast(Math.random() * angleLimit);
}
} else {
double dEqual = angleLimit / numRuns;
double current = 0;
for (int i = 0; i < numRuns; i++) {
runFast(current);
current += dEqual;
}
}
isSlow = true;
} else if (source.equals("reset")) {
reset();
dlp.remove(1);
dlp.add(new ScrollPanel(createCanvas(WIDTH, HEIGHT,
circleRadius, ballRadius, side)));
} else if (source.equals("clear")) {
for (int i = 0; i < path.length; i++)
removePath(i);
} else if (source.equals("update")) {
}
}
}
}
private void setAngle(IsCircle a, double theta) {
a.setDxy(Math.cos(theta), (Math.sin(theta)));
}
private void reset() {
pathNum = 0;
for (int i = 0; i < path.length; i++)
path[i].moveTo(WIDTH / 2, HEIGHT / 2);
ball.setVxy(WIDTH / 2, HEIGHT / 2);
ball.setDxy(0, 0);
// freq[hits]++;
hits = 0;
infoLabel.removeAllRows();
testLabel.removeAllRows();
createTableHeaders();
}
private void run(double angle) {
if (isSlow) {
if (infoLabel.getRowCount() > 1) {
infoLabel.removeAllRows();
testLabel.removeAllRows();
createTableHeaders();
}
if (pathNum + 1 == path.length) {
pathNum = 0;
} else {
pathNum++;
}
removePath(pathNum);
}
lastCircle.clear();
lastCircle.add(0);
initAngle = angle;
ball.setVxy(WIDTH / 2, HEIGHT / 2);
setAngle(ball, angle * Math.PI / 180);
while (moveObject(ball) != true) {
}
}
private void removePath(int pathNum) {
for (int i = 0; i < path.length; i++) {
while (path[pathNum].getStepCount() != 0) {
path[pathNum].removeStep(0);
}
path[pathNum].moveTo(WIDTH / 2, HEIGHT / 2);
}
}
private void createTableHeaders() {
infoLabel.setText(0, 0, "Hit # ");
infoLabel.setText(0, 1, "Time (ms)");
infoLabel.setText(0, 2, "X-coordinate (px)");
infoLabel.setText(0, 3, "Y-coordinate (px)");
infoLabel.setText(0, 4, "Time Gap (ms)");
testLabel.setText(0, 0, "Maximum Count");
testLabel.setText(0, 1, "Angle");
testLabel.setText(1, 0, "null");
testLabel.setText(1, 1, "null");
}
private boolean validate() {
boolean result;
try {
circleRadius = Integer.valueOf(circleRadiusBox.getText());
ballRadius = Integer.valueOf(ballRadiusBox.getText());
side = Integer.valueOf(sideBox.getText());
if (circleRadius >= side / 2) {
Window.alert("Circle radius must be less than side/2");
}
angle = Double.parseDouble(angleBox.getText());
if (angle >= 360) {
angleBox.setText(angle % 360 + "");
angle %= 360;
}
result = true;
} catch (Exception e) {
Window.alert("Please check inputs");
result = false;
}
return result;
}
public void runFast(double angle) {
hits = 0;
lastCircle.clear();
lastCircle.add(0);
initAngle = angle;
ballFast.setVxy(WIDTH / 2, HEIGHT / 2);
setAngle(ballFast, angle * Math.PI / 180);
while (moveObjectFast(ballFast) != true)
;
}
public boolean moveObjectFast(IsCircle b) {
boolean cancel = false;
// move
b.moveV();
// check collision
if (lastCircle.get(lastCircle.size() - 1) != 1
&& PinballMath.intersect(c1, ballFast) != -1) {
doPhysicsFast(c1, ballFast, 1);
} else if (lastCircle.get(lastCircle.size() - 1) != 2
&& PinballMath.intersect(c2, ballFast) != -1) {
doPhysicsFast(c2, ballFast, 2);
} else if (lastCircle.get(lastCircle.size() - 1) != 3
&& PinballMath.intersect(c3, ballFast) != -1) {
doPhysicsFast(c3, ballFast, 3);
} else {
freq[hits]++;
logMax();
cancel = true;
}
return cancel;
}
private void doPhysicsFast(IsCircle a, IsCircle b, int i) {
hits++;
lastCircle.add(i);
double intersectTime = PinballMath.intersect(a, b);
b.setVxy(b.getVx() + intersectTime * b.getDx(), b.getVy()
+ intersectTime * b.getDy());
double[] tempReflect = PinballMath.reflect(a, b);
b.setDxy(tempReflect[0], tempReflect[1]);
}
}